/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.widget;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import lt.marsrutai.mobile.android.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * <p>A view group which arranges it's contents in rows. Children which do not "fit" horizontally
 * in the current row are put in the next row.
 * </p>
 * <p>Originally from <a href="http://stackoverflow.com/a/4937713">http://stackoverflow.com/a/4937713</a>
 * with minor modifications.
 * </p>
 * 
 * @author Micah Hainline
 * @see <a href="http://stackoverflow.com/a/4937713">http://stackoverflow.com/a/4937713</a>
 */
public class RowLayout extends ViewGroup {
    
    public static final int DEFAULT_HORIZONTAL_SPACING = 5;
    public static final int DEFAULT_VERTICAL_SPACING = 5;
    
    private final int horizontalSpacing;
    private final int verticalSpacing;
    
    /**
     * Holds row measurements computed in measure phase and is used in layout phase
     * to position the children 
     */
    private List<RowMeasurement> currentRows = Collections.emptyList();

    public RowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.RowLayout);
        horizontalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.RowLayout_android_horizontalSpacing, DEFAULT_HORIZONTAL_SPACING);
        verticalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.RowLayout_android_verticalSpacing, DEFAULT_VERTICAL_SPACING);
        styledAttributes.recycle();
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int maxInternalWidth = MeasureSpec.getSize(widthMeasureSpec) - getHorizontalPadding();
        final int maxInternalHeight = MeasureSpec.getSize(heightMeasureSpec) - getVerticalPadding();
        
        // Assign all children to rows
        // This may create many short-lived objects which could impact performance (GC) - consider refactoring if need be
        List<RowMeasurement> rows = computeRowMeasurements(widthMode, heightMode, maxInternalWidth, maxInternalHeight);

        // Measure
        int longestRowWidth = 0;
        int totalRowHeight = 0;
        for (int index = 0; index < rows.size(); index++) {
            RowMeasurement row = rows.get(index);
            totalRowHeight += row.getHeight();
            if (index < rows.size() - 1) {
                totalRowHeight += verticalSpacing;
            }
            longestRowWidth = Math.max(longestRowWidth, row.getWidth());
        }
        
        // Set the computed width and height of this view
        setMeasuredDimension(
                widthMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(widthMeasureSpec) : longestRowWidth + getHorizontalPadding(),
                heightMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec) : totalRowHeight + getVerticalPadding());
        
        currentRows = Collections.unmodifiableList(rows);
    }

    /**
     * Measures all the children and computes row sizes so that the width of each row would not exceed
     * <code>maxInternalWidth</code>.
     * 
     * @param widthMode
     * @param heightMode
     * @param maxInternalWidth
     * @param maxInternalHeight
     * @return
     */
    private List<RowMeasurement> computeRowMeasurements(int widthMode, int heightMode, int maxInternalWidth, int maxInternalHeight) {
        List<RowMeasurement> rows = new ArrayList<RowMeasurement>();
        RowMeasurement currentRow = addNewRow(rows, widthMode, maxInternalWidth);
        
        for (View child : getLayoutChildren()) {
            measureChild(child, widthMode, heightMode, maxInternalWidth, maxInternalHeight);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            
            if (currentRow.wouldExceedMax(childWidth)) {
                currentRow = addNewRow(rows, widthMode, maxInternalWidth);
            }
            currentRow.addChildDimensions(childWidth, childHeight);
        }
        return rows;
    }

    private RowMeasurement addNewRow(List<RowMeasurement> rows, int widthMode, int maxInternalWidth) {
        RowMeasurement currentRow = new RowMeasurement(maxInternalWidth, widthMode);
        rows.add(currentRow);
        return currentRow;
    }

    /**
     * Measures provided <code>child</code> while taking into account various layout requirements
     * 
     * @param child
     * @param widthMode
     * @param heightMode
     * @param maxInternalWidth
     * @param maxInternalHeight
     */
    private void measureChild(View child, int widthMode, int heightMode, int maxInternalWidth, int maxInternalHeight) {
        LayoutParams childLayoutParams = child.getLayoutParams();
        int childWidthSpec = createChildMeasureSpec(childLayoutParams.width, maxInternalWidth, widthMode);
        int childHeightSpec = createChildMeasureSpec(childLayoutParams.height, maxInternalHeight, heightMode);
        child.measure(childWidthSpec, childHeightSpec);
    }

    @SuppressWarnings("deprecation")
    private int createChildMeasureSpec(int childLayoutParam, int max, int parentMode) {
        int spec;
        if (childLayoutParam == LayoutParams.FILL_PARENT) {
            spec = MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY);
        } else if (childLayoutParam == LayoutParams.WRAP_CONTENT) {
            spec = MeasureSpec.makeMeasureSpec(max, parentMode == MeasureSpec.UNSPECIFIED ? MeasureSpec.UNSPECIFIED : MeasureSpec.AT_MOST);
        } else {
            spec = MeasureSpec.makeMeasureSpec(childLayoutParam, MeasureSpec.EXACTLY);
        }
        return spec;
    }

    @Override
    protected void onLayout(boolean changed, int leftPosition, int topPosition, int rightPosition, int bottomPosition) {
        final int widthOffset = getMeasuredWidth() - getPaddingRight();
        int x = getPaddingLeft();
        int y = getPaddingTop();

        Iterator<RowMeasurement> rowIterator = currentRows.iterator();
        RowMeasurement currentRow = rowIterator.next();
        for (View child : getLayoutChildren()) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            if (x + childWidth > widthOffset) {
                x = getPaddingLeft();
                y += currentRow.height + verticalSpacing;
                if (rowIterator.hasNext()) {
                    currentRow = rowIterator.next();
                }
            }
            child.layout(x, y, x + childWidth, y + childHeight);
            x += childWidth + horizontalSpacing;
        }
    }

    private List<View> getLayoutChildren() {
        List<View> children = new ArrayList<View>();
        for (int index = 0; index < getChildCount(); index++) {
            View child = getChildAt(index);
            if (child.getVisibility() != View.GONE) {
                children.add(child);
            }
        }
        return children;
    }

    protected int getVerticalPadding() {
        return getPaddingTop() + getPaddingBottom();
    }

    protected int getHorizontalPadding() {
        return getPaddingLeft() + getPaddingRight();
    }

    private final class RowMeasurement {
        private final int maxWidth;
        private final int widthMode;
        private int width;
        private int height;

        public RowMeasurement(int maxWidth, int widthMode) {
            this.maxWidth = maxWidth;
            this.widthMode = widthMode;
        }

        public int getHeight() {
            return height;
        }

        public int getWidth() {
            return width;
        }

        public boolean wouldExceedMax(int childWidth) {
            return widthMode == MeasureSpec.UNSPECIFIED ? false : getNewWidth(childWidth) > maxWidth;
        }

        public void addChildDimensions(int childWidth, int childHeight) {
            width = getNewWidth(childWidth);
            height = Math.max(height, childHeight);
        }

        private int getNewWidth(int childWidth) {
            return width == 0 ? childWidth : width + horizontalSpacing + childWidth;
        }
    }
}
